for…of VS forEach VS for…in

Iterar sobre una lista es el pan de todos los días a la hora de estar programando.

Todos conocemos los famosos ejemplos con while, do while y for. (Aunque ahora que lo pienso no he usado do while desde hace años… pobre do while)

Pero… ¿sabías que Javascript tiene al menos 3 variantes al iterar utilizando el concepto de for? (Aparte del ciclo for común y corriente claro)

Es hora de entender exactamente que diferencia a cada uno y en que momentos querrías utilizar uno sobre otro.


for...of

La expresión de for...of fue introducida al lenguaje en la versión de ES6. Esta expresión ejecuta un ciclo que opera sobre la secuencia de valores de algun objeto Iterable.

¿Qué es un objeto Iterable?

Son todos los objetos de Javascript que implementan un protocolo de iteración de Javascript. En términos simples el protocolo consiste en proporcionar una función a cada objeto llamada Symbol.iterator.

Los objetos Iterable nativos del lenguaje ya contienen este protocolo implmentado. Estos son:

  • Array
  • String
  • TypedArray
  • Map
  • Set
  • NodeList

También son Iterables el objeto arguments y los generadores producidos por funciones generados.

*Adicionalmente tu como programador también puedes crear tus propios objetos y convertirlos a Iterables simplememente implementando el protocolo. En este post no ahondaremos en este tema pero es importante que sepas que es posible.

Tras bambalinas for...of hace uso de la función Symbol.iterator para iterar sobre cada elemento del objeto. Debido a la forma en la que esta implementado este te regresa el valor del elemento iterado y no tiene forma propia de devolverte un índice.

Veamos unos ejemplos:

Este código se encarga de imprimir los elementos dentro del arreglo spiderPeople, el string spidermanUncle y el Map spidermanLoveInterests:

const spiderPeople = ['Peter Parker', 'Gwen Stacy', 'Miles Morales']

const spidermanUncle = 'Ben Parker'

const spidermanLoveInterests = new Map()
spidermanLoveInterests.set('Gwen Stacy', 'deceased')
spidermanLoveInterests.set('Mary Jane', 'dating')


for (const element of spiderPeople) {
    console.log(element)
}
console.log('\n')

for (const element of spidermanUncle) {
    console.log(element)
}
console.log('\n')

for (const element of spidermanLoveInterests) {
    console.log(element)
}

El resultado es:

Peter Parker
Gwen Stacy
Miles Morales


B
e
n
 
P
a
r
k
e
r


[ 'Gwen Stacy', 'deceased' ]
[ 'Mary Jane', 'dating' ]

for...of es la expresión por excelencia a utilizar cuando necesite iterar sobre alguno de los elementos que mencioné como Iterables. Esto es porque aparte de poder iterar sobre una amplia variedad de objetos tiene mejor rendimiento que sus contrapartes.


forEach

forEach es a la vez diferente y parecido a for...of

Técnicamente podríamos considerarlo su antecesor, pero en vez de ser una expresión es un método que pertenece a la familia de Arrays.

Esto quiere decir que este método no se encuentra disponible en los otros objetos Iterables como: Map, Set, String, etc.

Y por lo tanto si utilizamos los mismos elementos que en el ejemplo de for...of solo debería funcionar con el arreglo:

const spiderPeople = ['Peter Parker', 'Gwen Stacy', 'Miles Morales']
const spidermanUncle = 'Ben Parker'
const spidermanLoveInterests = new Map()
spidermanLoveInterests.set('Gwen Stacy', 'deceased')
spidermanLoveInterests.set('Mary Jane', 'dating')

spiderPeople.forEach(function(element) {
    console.log(element)
})
console.log('\n')

spidermanUncle.forEach(function(element) {
    console.log(element)
})
console.log('\n')

spidermanLoveInterests.forEach(function(element) {
    console.log(element)
})
Peter Parker
Gwen Stacy
Miles Morales


d:\Documents\Projects\TeachingProjects\Javascript\javascriptDemos\src\allFors\foreach.js:13
spidermanUncle.forEach(function(element) {
               ^

TypeError: spidermanUncle.forEach is not a function
    at Object.<anonymous> (d:\Documents\Projects\TeachingProjects\Javascript\javascriptDemos\src\allFors\foreach.js:13:16)
    at Module._compile (node:internal/modules/cjs/loader:1254:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:23:47

Node.js v18.14.1

Ciertamente la terminal imprime correctamente el contenido del arreglo pero nos muestra un error posteriormente. Lo cual es comportamiento esperado.

Ahora que ya vimos esto…¿cuándo es recomendable utilizar forEach?

La sorprendente respuesta es… ¡NUNCA!

No es que forEach sea malo. Pero for...of es la mejor opción. Abarca mas tipos de objetos que solo los arreglos por lo que es mucho más versátil y aparte tiene mejor rendimiento. Aquí te dejo un artículo al respecto. (Esta en inglés pero no dejes que eso te espante)

Dicho todo esto. El hecho de que no sea el mejor tipo de bucle no va a evitar que te lo encuentres. Ya sea porque esta en código antiguo de Javascript o porque lo programó algun compañero que todavía no se haya informado de for...of como tu lo acabas de hacer 😉 .


for...in

Esta expresión se encarga de iterar sobre las propiedades de un objeto. Lo cual puede ser bastante útil en diversas situaciones. Sobre todo si por alguna razón no conoces las propiedades de un objeto o este cuenta con propiedades dinámicas.

La expresión te da acceso al índice o propiedad sobre el cuál estas iterando y no al valor como tal.

Veamos un ejemplo con un objeto:

const spiderman = {
    secretIdentity: "Peter Parker",
    powers: "enhanced strength, spider-sense, wall-crawling, spider-web",
    powersOrigin: "radioactive spider",
    loveInterests: ["Mary Jane", "Gwen Stacy"]
}

for (const property in spiderman) {
    console.log(`${property}:`, spiderman[property])
}

Al ejecutar este código la terminal nos arroja lo siguiente:

secretIdentity: Peter Parker
powers: enhanced strength, spider-sense, wall-crawling, spider-web
powersOrigin: radioactive spider
loveInterests: [ 'Mary Jane', 'Gwen Stacy' ]

Perfecto. Este es el comportamiento estándar.

Ahora. Recuerda que en Javascript todo es un objeto. Esto porsupuesto incluye a los arreglos y por lo tanto for...in también se utiliza para iterar sobre ellos.

const spiderPeople = ['Peter Parker', 'Gwen Stacy', 'Miles Morales']

for (const property in spiderPeople) {
    console.log(`${property}:`, spiderPeople[property])
}

Al correr este código se obtiene el siguiente resultado:

0: Peter Parker
1: Gwen Stacy
2: Miles Morales

Nada raro por el momento. Sin embargo, debes recordar que un arreglo puede tener propiedades adicionales así como lo hace un objeto. Esta característica abre la puerta a la posibilidad a que las cosas puedan salir terriblemente mal.

Imagina que hacemos una pequeña modificación al código anterior:

const spiderPeople = ['Peter Parker', 'Gwen Stacy', 'Miles Morales']
Object.getPrototypeOf(spiderPeople).extra = 'Peter Porker'

for (const property in spiderPeople) {
    console.log(`${property}:`, spiderPeople[property])
}

En este código estamos obteniendo el prototipo del arreglo y le estamos agregando una propiedad llamada extra.

¿Crees que la terminal imprima algo diferente?

0: Peter Parker
1: Gwen Stacy
2: Miles Morales
extra: Peter Porker

Como puedes ver…la respuesta es si. ¿Pero por qué pasa esto?

Para entender esto necesitas tener un entendimiento de como funcionan los prototipos en JS.

Sin embargo como resumen te puedo decir que JS define un objeto como prototipo. Posteriormente los objetos creados heredan las propiedades del mismo. Y por lo tanto si modificas las propiedades del objeto prototipo los objetos hijos serán afectados también.

Como mencioné antes los arreglos son objetos. Objetos especiales pero objetos al fin y al cabo. La particularidad que tienen es que internamente cada elemento es una propiedad y por ende para for...in los elementos del arreglo y las propiedades son lo mismo y es por eso que se imprimen los 4 valores.

Si no terminas de entender este tema no te preocupes. Como mencioné, es un tema algo complejo. Por lo que te recomiendo que revises mas acerca del tema de prototipos: ¿Qué es el prototype en JS?

Por otro lado si sientes que necesitas mejorar tus bases de Javascript te recomiendo estos otros artículos:


Bonus

for...in también nos permite iterar sobre un string.

const spiderman = "Spider-man"
Object.getPrototypeOf(spiderman).extra = 'Peter Porker'

for (const property in spiderman) {
    console.log(`${property}:`, spiderman[property])
}
0: S
1: p
2: i
3: d
4: e
5: r
6: -
7: m
8: a
9: n
extra: Peter Porker

Si bien nos da el mismo problema que el arreglo. Es interesante ver que las propiedades que se muestran tratan a un string como si se tratara de un arreglo también. Un arreglo de carácteres.

Hay muchas cosas que uno puede aprender aunque no sean parte del tema principal…ahora sabemos que un string en Javascript se maneja internamente como un arreglo.